package controller;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;

import networkInterface.ChatNI;
import networkInterface.InetAddressUtil;
import networkInterface.Utilisateur;
import protocol.AckFile;
import protocol.Bye;
import protocol.ChatMessage;
import protocol.FileNotify;
import protocol.Hello;
import protocol.Message;

/**
 * Classe ChatController, controleur et modele du pattern MVC
 * 
 * @author Flo
 * 
 */
public class ChatController {

	// ********************************************************************
	// Attributs de classe
	// ********************************************************************

	/**
	 * Booleen BAVARD pour tester et ecrire des commentaires dans la console
	 */
	private final static boolean BAVARD = true;

	// ********************************************************************
	// Attributs d'instance
	// ********************************************************************

	/**
	 * {@link Utilisateur} qui utilise le Chat
	 */
	private Utilisateur moi;
	/**
	 * Liste des utilisateurs connectes
	 */
	private ArrayList<Utilisateur> copains;
	/**
	 * Instance de {@link ChatNI} utilisee pour connecter ce chat aux autres
	 */
	private ChatNI chatNi;
	/**
	 * Booleen a true si on est connecte
	 */
	private boolean connected;
	/**
	 * le copain, selectionne actuellement, a qui on parle ou on envoie un
	 * fichier
	 */
	private Utilisateur copainCourant;
	/**
	 * Writer pour ecrire et tester
	 */
	private Writer sortie;
	/**
	 * instance de l'interface Refreshable qui doit etre rafraichie quand
	 * certains evenements se produisent
	 */
	private Refreshable aRafraichir;
	/**
	 * La liste des fichiers a accepter ou annuler dans leur ordre d'arrivee
	 */
	private ArrayList<FileNotify> fileEnAttente;
	/**
	 * Le Fichier a envoyer
	 */
	private File fileAEnvoyer;
	/**
	 * Le destinataire du fichier qui a ete selectionne (peut etre different du
	 * copainCourant)
	 */
	private Utilisateur destinataireFichier;

	// ********************************************************************
	// Constructeur et initialisation
	// ********************************************************************

	/**
	 * Constructeur ChatController
	 * 
	 * @param aRafraichir
	 *            instance de l'interface {@link Refreshable}, que le controleur
	 *            pourra rafraichir quand il changera d'état (arrivée d'un
	 *            message, etc)
	 */
	public ChatController(Refreshable aRafraichir) {
		this.aRafraichir = aRafraichir;
	}

	/**
	 * initialise tous les attributs de la classe ChatController
	 * 
	 * @param nom
	 *            le pseudo de l'utilisateur
	 * @param port
	 *            le port utilise par l'utilisateur
	 * @param sortie
	 *            un Writer de sortie
	 */
	public void init(String nom, int port, Writer sortie) {
		// Recuperation de l'adresse locale
		InetAddress adresse = null;
		try {
			adresse = InetAddress.getLocalHost();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		this.moi = new Utilisateur(nom, adresse, port);
		this.copains = new ArrayList<Utilisateur>();
		this.chatNi = new ChatNI();
		this.connected = false;
		this.fileEnAttente = new ArrayList<FileNotify>();
		if (ChatController.BAVARD)
			System.out.println("     ChatController -- Creation du chat : "
					+ this.getNom());
		this.sortie = sortie;
	}

	// ********************************************************************
	// Getters des attributs de ChatController
	// ********************************************************************

	/**
	 * getter Nom
	 * 
	 * @return le nom de l'Utilisateur
	 */
	private String getNom() {
		return this.moi.getNom();
	}

	/**
	 * getter Port
	 * 
	 * @return le port de l'Utilisateur
	 */
	private int getPort() {
		return this.moi.getPort();
	}

	/**
	 * getter liste de copains
	 * 
	 * @return copains la liste de copains actuels
	 */
	public ArrayList<Utilisateur> getCopains() {
		return this.copains;
	}

	/**
	 * getter liste fichiers a accepter ou annuler
	 * 
	 * @return fileEnAttente
	 */
	public ArrayList<FileNotify> getFilesEnAttente() {
		return this.fileEnAttente;
	}

	/**
	 * Getter du copain courant
	 * 
	 * @return le copain courant, ou null si aucun copain n'est selectionne
	 */
	public Utilisateur getCopainCourant() {
		return this.copainCourant;
	}

	// ********************************************************************
	// Methodes relatives aux copains (recherche, affichage, selection)
	// ********************************************************************

	/**
	 * Affichage de la liste de copains courants
	 */
	private void printCopains() {
		System.out.println("\n----------------------------------------");
		System.out.println("Voici la nouvelle liste de mes copains :");
		if (this.getCopains().isEmpty()) {
			System.out.println("\tJ'ai pas de copains...");
		} else {
			for (Utilisateur copain : this.getCopains()) {
				System.out.println("\t" + copain.toString());
			}
		}
		System.out.println("----------------------------------------\n");
	}

	/**
	 * 
	 * @param nomCopain
	 *            le pseudo du copain a trouver
	 * @return le copain qui a ete trouve
	 */
	private Utilisateur trouverCopainParNom(String nomCopain) {
		for (Utilisateur copain : this.getCopains()) {
			if (copain.getNom().equals(nomCopain)) {
				return copain;
			}
		}
		// Si on arrive ici, pas de copain de ce nom
		return null;
	}

	/**
	 * 
	 * @param adresse
	 * @param port
	 * @return le copain qui a ete trouve a partir de son port et de son adresse
	 */
	private Utilisateur trouverCopainParAdresseEtPort(InetAddress adresse,
			int port) {
		for (Utilisateur copain : this.getCopains()) {
			if ((copain.getAddress().equals(adresse))
					&& (copain.getPort() == port)) {
				return copain;
			}
		}
		// Si on arrive ici, pas de copain avec cette adresse et ce port
		return null;
	}

	/**
	 * 
	 * @param nomCopain
	 *            le copain qu'on selectionne
	 * @return true si le copain a ete correctement selectionne
	 */
	public boolean choisirCopainCourant(String nomCopain) {
		Utilisateur copain = this.trouverCopainParNom(nomCopain);
		if (copain != null && !copain.equals(this.copainCourant)) {
			this.copainCourant = copain;
			System.out
					.println("     ChatController -- Nouveau copain courant : "
							+ copain.getNom());
		}
		return this.copainCourant != null;
	}

	/**
	 * 
	 * @param adresse
	 *            du copain a selectionner
	 * @param port
	 *            du copain
	 * @return true si le copain a ete correctement selectionne
	 */
	public boolean choisirCopainCourant(InetAddress adresse, int port) {
		this.copainCourant = this.trouverCopainParAdresseEtPort(adresse, port);
		return this.copainCourant != null;
	}

	// ********************************************************************
	// Methodes d'envoi
	// ********************************************************************

	/**
	 * envoi d'un hello a un copain
	 * 
	 * @param address
	 *            l'adresse du destinataire
	 * @param port
	 *            port udp sur lequel est base le chat
	 * @param repreq
	 *            reponse requise true si c'est la premiere fois qu'on envoie un
	 *            hello ou false si c'est une reponse a la connexion d'un copain
	 */
	public void envoyerHello(InetAddress address, int port, boolean repreq) {
		Hello hello = new Hello(repreq, this.moi.getNom(),
				this.moi.getAddress());
		this.chatNi.envoyer(hello, address, port);
	}

	/**
	 * envoi d'un hello en broadcast
	 * 
	 * @param port
	 *            port udp sur lequel est base le chat
	 * @param repreq
	 *            reponse requise true si c'est la premiere fois qu'on envoie un
	 *            hello ou false si c'est une reponse a la connexion d'un copain
	 */
	public void envoyerHello(int port, boolean repreq) {
		try {
			this.envoyerHello(InetAddressUtil.getLocalBroadcast(), port, repreq);
		} catch (UnknownHostException e) {
			e.printStackTrace();
			System.err.println("Problème de broadcast en réseau local");
		}
	}

	/**
	 * envoi d'un Bye a chaque copain
	 */
	public void envoyerBye() {
		// On dit au revoir a chaque copain
		Bye bye = new Bye(this.moi.getNom(), this.moi.getAddress());
		for (Utilisateur copain : this.getCopains()) {
			this.chatNi.envoyer(bye, copain.getAddress(), copain.getPort());
		}
		// On oublie les copains
		this.getCopains().clear();
	}

	/**
	 * envoi d'un fichier
	 * 
	 * @param file
	 *            le fichier a envoyer
	 */
	public void envoyerFileNotify(File file) {
		this.fileAEnvoyer = file;
		this.destinataireFichier = this.copainCourant;
		FileNotify notify = new FileNotify(this.moi.getNom(),
				this.moi.getAddress(), file.getName(), file.length());
		this.chatNi.envoyer(notify, this.destinataireFichier.getAddress(),
				this.destinataireFichier.getPort());
		System.out
				.println("     ChatController -- Proposition d'envoi du fichier "
						+ file.getName()
						+ " à "
						+ this.destinataireFichier.getNom());
	}

	/**
	 * 
	 * @param adresse
	 *            adresse du destinataire
	 * @param ok
	 *            booleen true si on accepte de recevoir le fichier, false si on
	 *            refuse
	 * @param fichier
	 *            le fichier en question
	 */
	public void envoyerAck(InetAddress adresse, boolean ok, File fichier) {
		AckFile ack = new AckFile(this.moi.getNom(), this.moi.getAddress(), ok);
		// On ne recoit le fichier que si on a dit ok
		if (ok) {
			this.chatNi.recevoirFichierParTCP(fichier);
		}
		// PUIS on envoie l'ack (si on l'envoie avant, la mise en place du
		// serveur risque de prendre plus de temps que la connexion du
		// destinataire a notre serveur)
		this.chatNi.envoyer(ack, adresse, this.moi.getPort());
		System.out
				.println("     ChatController -- Réponse à proposition d'envoi : "
						+ ok);
		// On rafraichit l'ihm (les boutons accept et cancel peuvent redevenir
		// gris)
		this.aRafraichir.refresh();
	}

	/**
	 * 
	 * @param texte
	 *            la conversation qu'on envoie
	 * @return false si le copain n'a pas ete selectionne au prealable
	 */
	public boolean envoyerTexte(String texte) {
		if (this.copainCourant == null) {
			return false;
		}
		ChatMessage message = new ChatMessage(this.moi.getNom(), texte,
				this.moi.getAddress());
		// Rappel : on parle au copain courant
		this.chatNi.envoyer(message, this.copainCourant.getAddress(),
				this.copainCourant.getPort());
		String texteSortie = "> Je dis à " + this.copainCourant.getNom()
				+ " : " + texte;
		ecrireDansSortie(texteSortie);
		return true;
	}

	// ********************************************************************
	// Methodes de connexion et deconnexion
	// ********************************************************************

	/**
	 * se connecter, methode qui permet d'etre connecte au chat, c'est ici que
	 * les threads de reception de paquets et de traitement des paquets sont
	 * lances, envoi du hello en broadcast
	 */
	public void connecter() {
		this.connected = this.chatNi.connecter(this.getPort());

		if (this.connected) {

			// Creation du thread de reception
			if (ChatController.BAVARD)
				System.out
						.println("     ChatController -- Tentative de connection de "
								+ this.getNom());
			Thread threadReception = new Thread() {

				@Override
				public void run() {
					while (ChatController.this.connected) {
						// Reception d'un message par la couche reseau
						try {
							ChatController.this.chatNi.recevoir();
						} catch (Exception e) {
							// Le socket a ete detruit entre temps, on ne fait
							// rien
							if (ChatController.BAVARD)
								System.out
										.println("     ChatController -- Arrivée d'un paquet, mais c'est trop tard, la connexion est fermée");
							return;
						}
					}
				}
			};
			Thread threadMessageEnAttente = new Thread() {
				public void run() {
					while (ChatController.this.connected) {
						ChatController.this.traiterMessage();

					}
				}
			};
			threadReception.start();
			threadMessageEnAttente.start();
			if (ChatController.BAVARD)
				System.out.println("     ChatController -- " + this.getNom()
						+ " est connecté sur le port " + this.getPort() + " !");
		}
		this.envoyerHello(this.moi.getPort(), true);

		String texte = "-------------------------------\n  + Connexion";
		ecrireDansSortie(texte);
	}

	/**
	 * envoyer un bye et se deconnecter du chat
	 */
	public void deconnecter() {
		// On est poli, on dit au revoir avant de partir
		if (ChatController.BAVARD)
			System.out.println("     ChatController -- Déconnexion de "
					+ this.getNom());
		this.envoyerBye();
		// Passage en deconnecte.
		this.connected = false;
		this.chatNi.deconnecter();
		this.fileAEnvoyer = null;
		this.fileEnAttente.clear();
		this.copainCourant = null;
		this.destinataireFichier = null;
		if (ChatController.BAVARD)
			System.out.println("     ChatController -- " + this.getNom()
					+ " est déconnecté");
		String texte = "  - Déconnexion";
		ecrireDansSortie(texte);
	}

	// ********************************************************************
	// Methodes de reception et interpretation de message
	// ********************************************************************

	/**
	 * gere la file des messages en attente, les traite un a un
	 */
	public void traiterMessage() {
		ArrayList<Message> messageEnAttente = this.chatNi.getMessageEnAttente();
		if (messageEnAttente.isEmpty()) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return;
		}
		Message messageATraiter = messageEnAttente.remove(0);
		interpreteMessage(messageATraiter);
	}

	/**
	 * interprete le message et agit en consequence selon la nature de message,
	 * fonction tres importante car c'est a ce niveau que les messages sont
	 * dissocies
	 * 
	 * @param message
	 *            message a interpreter selon sa nature
	 */
	public void interpreteMessage(Message message) {
		// Interpretation du message

		// Si on n'est pas connecte (qu'on s'est deconnecte en attendant le
		// message), on ne fait rien
		if (!this.connected || message == null) {
			return;
		}

		System.err
				.println("     ChatController -- Réception d'un nouveau message, début de l'interprétation...");
		// Hello
		if (message instanceof Hello) {
			Hello hello = (Hello) message;
			// L'emetteur
			Utilisateur copain = new Utilisateur(hello.getUsername(),
					hello.getSourceAddress(), this.moi.getPort());

			if (this.moi.getAddress().equals(copain.getAddress())
					&& this.moi.getPort() == copain.getPort()) {
				// On ne fait rien : on vient juste de recevoir notre
				// propre broadcast...
				System.err
						.println("     ChatController -- Reception d'un Hello de moi meme...");
				return;
			}

			if (this.getCopains().contains(copain) || copain.equals(this.moi)) {
				// Cas ou l'emetteur veut un nom deja pris : on teste qu'on n'a
				// pas deja un ami qui porte ce nom, MAIS AUSSI qu'il ne veut
				// pas NOTRE nom !
				System.err
						.println("     ChatController -- Reception d'un Hello d'un usurpateur d'identité : "
								+ copain.getNom() + " !");
				return;
			}

			System.out
					.println("     ChatController -- Reception d'un Hello de "
							+ copain.getNom());
			
			// Cas nominal : un nouveau copain, que l'on memorise 
			this.getCopains().add(copain);
			// On rafraichit l'ihm
			this.aRafraichir.refresh();

			String texte = " *** Connexion de " + hello.getUsername() + " ***";
			ecrireDansSortie(texte);

			if (ChatController.BAVARD)
				System.out
						.println("     ChatController -- Super ! J'ai un nouveau copain : "
								+ copain.toString());
			// On affiche la liste des copains
			this.printCopains();
			// Si une reponse est requise...
			if (hello.isRepreq()) {
				// On attend 10 millisecondes
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				// On repond
				this.envoyerHello(copain.getAddress(), copain.getPort(), false);
			}
		}
		// Bye
		if (message instanceof Bye) {
			Bye bye = (Bye) message;
			String texte = " *** Déconnexion de " + bye.getUsername() + " ***";
			ecrireDansSortie(texte);
			// On cherche le copain qui nous dit au revoir
			String nomCopain = bye.getUsername();
			Utilisateur copainOrigine = trouverCopainParNom(nomCopain);
			// Si on connaissait ce copain, on l'oublie
			if (copainOrigine != null) {
				this.getCopains().remove(copainOrigine);
				if (this.copainCourant != null
						&& this.copainCourant.equals(copainOrigine)) {
					this.copainCourant = null;
				}
				this.aRafraichir.refresh();
				if (ChatController.BAVARD)
					System.out
							.println("     ChatController -- Bouh ! J'ai perdu un copain : "
									+ copainOrigine.toString());
			}

			// On affiche la liste des copains
			this.printCopains();
		}
		// ChatMessage
		if (message instanceof ChatMessage) {
			ChatMessage chatMessage = (ChatMessage) message;
			// On cherche le copain emetteur
			Utilisateur copainOrigine = trouverCopainParAdresseEtPort(
					chatMessage.getSourceAddress(), this.moi.getPort());
			String texte = chatMessage.getMessage();
			System.out.println("     ChatController -- C'est du texte : "
					+ texte);
			// Si on connait le copain :
			if (copainOrigine != null) {
				// On dit que c'est lui qui parle
				texte = "< " + copainOrigine.getNom() + " a dit : " + texte;
			} else {
				// Sinon, c'est un inconnu (autant ne pas perdre le message...)
				texte = "< Un inconnu a dit : " + texte;
			}
			ecrireDansSortie(texte);
		}
		// FileNotify
		if (message instanceof FileNotify) {
			FileNotify fileNot = (FileNotify) message;
			System.out.println("     ChatController -- C'est un FileNotify");
			// On stocke la proposition de fichier dans la file d'attente dediee
			this.fileEnAttente.add(fileNot);
			String texte = " *** Notification du fichier "
					+ fileNot.getNomFichier() + " de taille "
					+ fileNot.getTailleFichier() + " octets envoyé par "
					+ fileNot.getUsername() + " ***";
			ecrireDansSortie(texte);
			// On rafraichit l'ihm (pour prevenir de la proposition de fichier)
			this.aRafraichir.refresh();
		}
		// AckFile
		if (message instanceof AckFile) {
			AckFile ack = (AckFile) message;
			System.out.println("     ChatController -- C'est un AckFile");
			// Si le copain accepte le fichier :
			if (ack.isAck()) {
				String texte = " *** Envoi du fichier "
						+ this.fileAEnvoyer.getName() + " accepté par "
						+ destinataireFichier.getNom() + " ***";
				ecrireDansSortie(texte);
				// On envoie le fichier
				this.chatNi.envoyerFichierParTCP(
						destinataireFichier.getAddress(), this.fileAEnvoyer);
			} else {
				String texte = " *** Envoi du fichier "
						+ this.fileAEnvoyer.getName() + " annulé par "
						+ destinataireFichier.getNom() + " ***";
				ecrireDansSortie(texte);
				// Sinon on ne fait rien
				this.fileAEnvoyer = null;
				this.destinataireFichier = null;
			}
		}
	}

	// ********************************************************************
	// Methodes annexes
	// ********************************************************************

	/**
	 * 
	 * @param texte
	 *            a ecrire dans l'interface
	 */
	private void ecrireDansSortie(String texte) {
		try {
			this.sortie.write(texte + "\n");
			this.sortie.flush();
		} catch (IOException e) {
			System.err
					.println("Problème d'écriture dans la sortie, du texte : \""
							+ texte + "\"");
			e.printStackTrace();
		}
	}

}
